﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Text;
using System.IO;
using System.Globalization;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
using System.Diagnostics;

namespace BizElements.TemplateEngine
{
    /// <summary>Read-only metadata specified for XML template.</summary>
    public interface ITemplateData
    {
        // Note: Variables are not read-only!!!

        #region Properties.

        /// <summary>Template header.</summary>
        TemplateHeader Header { get; }

        /// <summary>Namespaces from which types are imported and used in template expressions.</summary>
        string[] Namespaces { get; }

        /// <summary>Assemblies required to run the template.</summary>
        string[] ReferencedAssemblies { get; }

        /// <summary>Template segments.</summary>
        IEnumerable<TemplateSegment> Segments { get; }

        /// <summary>Additional template settings which affect how the code is generated.</summary>
        IDictionary<string, TemplateSetting> Settings { get; }

        /// <summary>Template variables.</summary>
        IDictionary<string, TemplateVariableInfo> Variables { get; }

        /// <summary>Component variable definitions/metadata specified in this template.</summary>
        IEnumerable<TemplateComponentVariableDefinition> ComponentVariableDefinitions { get; }

        /// <summary>Values of all component variables.</summary>
        /// <value>Two level dictionary. 1st level key determines a metada source object on which input components are base, and 2nd level key determines variable.</value>
        /// <remarks>Metada source objects are determined by component keys - usually a table or class name. 
        /// Input components of different types, but based on same source object (table, class...).</remarks>
        IDictionary<string, IDictionary<string, string>> AllComponentVariables { get; set; }

        #endregion

        #region Methods.

        /// <summary>Gets the value stored in the specified variable.</summary>
        /// <param name="variableName">Variable name.</param>
        /// <returns>Value stored in the specified variable; <b>null</b> if the specified variable is not found.</returns>
        string GetVariable(string variableName);

        /// <summary>Gets the value associated with the specified settings key.</summary>
        /// <param name="settingKey">Settings key.</param>
        /// <returns>Value associated with the specified settings key; <b>null</b> if the specified setting is not found.</returns>
        string GetSetting(string settingKey);

        #endregion
    }

    /// <summary>Generates code based on XML/C# templates.</summary>
    public interface ITemplate : ITemplateData
    {
        #region XML.

        /// <summary>Raw XML content.</summary>
        string XmlContent { get; }
        
        /// <summary>Reads template from the specified XML file.</summary>
        /// <param name="fileName">The filename (including the path) from which to read.</param>
        void ReadXml(string fileName);        

        #endregion

        #region ValidateInput.

        /// <summary>Validates if provided input component matches input component criteria defined in XML file.</summary>
        bool IsValidInputComponent(ITemplateInputComponent input);

        /// <summary>Validates if provided object matches input component criteria defined in XML file.</summary>
        bool IsValidInputComponent(object obj);

        #endregion

        #region Run.

        /// <summary>Generates code for the given input component.</summary>
        void Run(StreamWriter output, ITemplateInputComponent input);

        /// <summary>Generates code for the given object.</summary>
        void Run(StreamWriter output, object obj);

        /// <summary>Generates code for the given input component.</summary>
        string Run(ITemplateInputComponent input);

        /// <summary>Generates code for the given object.</summary>
        string Run(object obj);
               
        #endregion

        #region Component variables.

        /// <summary>Gets all component variables applicable to specified input component.</summary>
        /// <param name="inputComponent">Input component.</param>
        /// <returns>A collection of component variables.</returns>
        IEnumerable<TemplateComponentVariableDefinition> GetComponentVariablesUsedFor(ITemplateInputComponent inputComponent);

        /// <summary>Checks whether a component variable/parameter is applicable to the specified input component.</summary>
        /// <param name="componentVariable">Component variable.</param>
        /// <param name="inputComponent">Input component to be tested againts used for boolean expression specifed in XML template.</param>
        /// <returns>Value returned by expression. <b>true</b> if expression is not defined in XML template.</returns>
        bool IsComponentVariableUsedFor(TemplateComponentVariableDefinition componentVariable, ITemplateInputComponent inputComponent);

        /// <summary>Checks whether a component variable/parameter is applicable to the specified input component.</summary>
        /// <param name="componentVariableName">Component variable name.</param>
        /// <param name="inputComponent">Input component to be tested againts used for boolean expression specifed in XML template.</param>
        /// <returns>Value returned by expression. <b>true</b> if expression is not defined in XML template or if the specified variable does not exist.</returns>
        bool IsComponentVariableUsedFor(string componentVariableName, ITemplateInputComponent inputComponent);

        #endregion
    }

    /// <summary>Generates code based on XML/C# templates.</summary>
    public sealed class Template : ITemplate
    {
        #region ExpressionDelimiter = '$'

        /// <summary>C# expressions in template body, which are to be compiled and evaluated during generation, are placed between $ symbols.</summary>
        /// <remarks>Dollar sign may not be used in template body, ie. segments. To write dollar signs, the following workaround is recommended.
        /// Create the following setting in XML template:
        /// <code>
        /// &lt;Setting&gt;
        ///     &lt;Name&gt;DollarSign&lt;/Name&gt;
        ///     &lt;Description&gt;String constant ($). Required because dollar character may not be used in template body.&lt;/Description&gt;
        ///     &lt;Value&gt;$&lt;/Value&gt;
        /// &lt;/Setting&gt;
        /// </code>
        /// The setting value may be used/written in template body by writing the foloowing expression:
        /// <code>$Settings["DollarSign"]$</code></remarks>
        public const char ExpressionDelimiter = '$';

        #endregion

        #region XML fields and properties.

        /// <summary>Gets XML template content/source.</summary>
        internal TextLines XmlLines { get; private set; }

        /// <summary>Gets template header.</summary>
        public TemplateHeader Header { get; internal set; }

        TemplateSegmentCollection segments;

        /// <summary>Gets all template segments, including root segment.</summary>
        public IEnumerable<TemplateSegment> Segments 
        {
            get { return this.segments.Segments; }
        }

        /// <summary>Gets template variables.</summary>
        public IDictionary<string, TemplateVariableInfo> Variables { get; internal set; }

        /// <summary>Gets additional template settings which affect how the code is generated.</summary>
        public IDictionary<string, TemplateSetting> Settings { get; private set; }                

        /// <summary>Gets assemblies required to run the template.</summary>
        public string[] ReferencedAssemblies { get; private set; }

        /// <summary>Gets namespaces from which types are imported and used in template expressions.</summary>
        public string[] Namespaces { get; private set; }

        ITemplateInputComponentBuilder componentBuilder;

        /// <summary>Gets raw XML content.</summary>
        public string XmlContent
        {
            get { return this.XmlLines.Text; }
        }

        /// <summary>Gets optional compiler settings for template expression if configured in XML.</summary>
        public CodeDomProviderInfo CodeDomProvider { get; private set; }

        /// <summary>Gets optional criteria that input component must satisfy. Otherwise template will not run.</summary>
        public InputComponentCriteriaInfo InputComponentCriteria { get; private set; }

        /// <summary>Gets component variable definitions/metadata specified in this template.</summary>
        public IEnumerable<TemplateComponentVariableDefinition> ComponentVariableDefinitions { get; private set; }

        #endregion

        /// <summary>Gets or sets values of all component variables.</summary>
        /// <value>Two level dictionary. 1st level key determines a metada source object on which input components are base, and 2nd level key determines variable.</value>
        /// <remarks>Metada source objects are determined by component keys - usually a table or class name. 
        /// Input components of different types, but based on same source object (table, class...).</remarks>
        public IDictionary<string, IDictionary<string, string>> AllComponentVariables { get; set; }

        #region ReadXml.

        /// <summary>Reads template from the specified XML file.</summary>
        /// <param name="fileName">The filename (including the path) from which to read.</param>
        public void ReadXml(string fileName)
        {
            using (FileStream file = new FileStream(fileName, FileMode.Open, FileAccess.Read))
            {
                this.XmlLines = TextLines.Read(file);
            }

            TemplateDef xmlTemplate;
            using (FileStream file = new FileStream(fileName, FileMode.Open, FileAccess.Read))
            {
                XmlSerializer serializer = new XmlSerializer(typeof(TemplateDef));
                xmlTemplate = (TemplateDef)serializer.Deserialize(file);
            }

            ReadDeserializedXmlTemplateDef(xmlTemplate);
        }

        private void ReadDeserializedXmlTemplateDef(TemplateDef xmlTemplate)
        {
            ReadHeader(xmlTemplate);
            ReadVariables(xmlTemplate);
            ReadSettings(xmlTemplate);
            ReadSegments(xmlTemplate);
            ReadComponentVariableDefinitions(xmlTemplate);
            ReadReferencedAssemblies(xmlTemplate);
            ReadNamespaces(xmlTemplate);
            ReadAndBuildComponentBuilder(xmlTemplate);
            ReadAndBuildCodeDomProvider(xmlTemplate);
            ReadAndBuildInputComponentCriteria(xmlTemplate);
        }

        private void ReadHeader(TemplateDef xmlTemplate)
        {
            this.Header = new TemplateHeader() { Description = xmlTemplate.Header.Description, Family = xmlTemplate.Header.Family.ToString(), Title = xmlTemplate.Header.Title };
        }

        private void ReadVariables(TemplateDef xmlTemplate)
        {
            this.Variables = new Dictionary<string, TemplateVariableInfo>();
            if (xmlTemplate.Variables != null)
            {
                foreach (VariableDef xmlVar in xmlTemplate.Variables)
                {
                    TemplateVariableInfo var;
                    if (xmlVar.AllowedValues != null)
                    {
                        NameValuePair[] allowedValues = new NameValuePair[xmlVar.AllowedValues.Length];
                        for (int i = 0; i < allowedValues.Length; i++)
                            allowedValues[i] = new NameValuePair(xmlVar.AllowedValues[i].Name, xmlVar.AllowedValues[i].Value);

                        var = new TemplateVariableInfo(xmlVar.Name, xmlVar.DefaultValue, xmlVar.Description, allowedValues);
                    }
                    else
                    {
                        var = new TemplateVariableInfo(xmlVar.Name, xmlVar.DefaultValue, xmlVar.Description);
                    }

                    this.Variables[var.Name] = var;
                }
            }
        }

        private void ReadSettings(TemplateDef xmlTemplate)
        {
            this.Settings = new Dictionary<string, TemplateSetting>();
            if (xmlTemplate.Settings != null)
            {
                foreach (SettingDef xmlSetting in xmlTemplate.Settings)
                {
                    TemplateSetting setting = new TemplateSetting() { Name = xmlSetting.Name, Value = xmlSetting.Value, Description = xmlSetting.Description };
                    this.Settings[setting.Name] = setting;
                }
            }
        }        

        private void ReadComponentVariableDefinitions(TemplateDef xmlTemplate)
        {
            List<TemplateComponentVariableDefinition> componentVariableDefinitions = new List<TemplateComponentVariableDefinition>();
            if (xmlTemplate.ComponentVariables != null)
            {
                foreach (ComponentVariableDef xmlVar in xmlTemplate.ComponentVariables)
                {
                    TemplateComponentVariableDefinition var;
                    if (xmlVar.AllowedValues != null)
                    {
                        NameValuePair[] allowedValues = new NameValuePair[xmlVar.AllowedValues.Length];
                        for (int i = 0; i < allowedValues.Length; i++)
                            allowedValues[i] = new NameValuePair(xmlVar.AllowedValues[i].Name, xmlVar.AllowedValues[i].Value);

                        var = new TemplateComponentVariableDefinition(xmlVar.Name, xmlVar.DefaultValue, xmlVar.Description, allowedValues);
                    }
                    else
                    {
                        var = new TemplateComponentVariableDefinition(xmlVar.Name, xmlVar.DefaultValue, xmlVar.Description);
                    }

                    if ((xmlVar.UsedFor != null))
                    {
                        string boolExpression = TextUtil.Trim(xmlVar.UsedFor.BooleanExpression);
                        if (!string.IsNullOrWhiteSpace(boolExpression))
                            var.UsedForBooleanExpression = xmlVar.UsedFor.BooleanExpression;
                    }

                    componentVariableDefinitions.Add(var);
                }
            }

            this.ComponentVariableDefinitions = componentVariableDefinitions;
        }

        private void ReadSegments(TemplateDef xmlTemplate)
        {
            // At least one segment must exist.
            List<TemplateSegment> segments = new List<TemplateSegment>();
            foreach (TemplateSegmentDef xmlSeg in xmlTemplate.TemplateSegments)
            {
                // XSD.EXE generates array of string for Text property because mixed="true" is defined in XSD.
                // HACK: XmlDeserializer normalizes all whitespace including new lines.
                string bodyWithCrLf = xmlSeg.Text[0].Replace("\n", "\r\n");
                bool debug = xmlSeg.DebugSpecified && xmlSeg.Debug;
                TemplateSegment segment = new TemplateSegment() { Id = xmlSeg.Id, Debug = debug, DebugIfInputName = xmlSeg.DebugIfInputName, Body = bodyWithCrLf };
                segments.Add(segment);
            }
            this.segments = new TemplateSegmentCollection(segments);
        }

        private void ReadReferencedAssemblies(TemplateDef xmlTemplate)
        {
            List<string> assemblies = new List<string>();
            if (xmlTemplate.ReferencedAssemblies != null)
            {
                foreach (string asm in xmlTemplate.ReferencedAssemblies)
                    assemblies.Add(asm);
            }
            this.ReferencedAssemblies = assemblies.ToArray();
        }

        private void ReadNamespaces(TemplateDef xmlTemplate)
        {
            List<string> namespaces = new List<string>();
            if (xmlTemplate.Namespaces != null)
            {
                foreach (string name in xmlTemplate.Namespaces)
                    namespaces.Add(name);
            }
            this.Namespaces = namespaces.ToArray();
        }

        private void ReadAndBuildComponentBuilder(TemplateDef xmlTemplate)
        {
            this.componentBuilder = null;
            if (xmlTemplate.InputComponentBuilder != null)
            {
                if (!string.IsNullOrEmpty(xmlTemplate.InputComponentBuilder.Assembly) && !string.IsNullOrEmpty(xmlTemplate.InputComponentBuilder.Class))
                    this.componentBuilder = (ITemplateInputComponentBuilder)ReflectionUtil.CreateInstance(xmlTemplate.InputComponentBuilder.Assembly, xmlTemplate.InputComponentBuilder.Class);
            }
        }

        private void ReadAndBuildCodeDomProvider(TemplateDef xmlTemplate)
        {
            this.CodeDomProvider = null;
            if (xmlTemplate.CodeDomProvider != null)
            {
                if (!string.IsNullOrEmpty(xmlTemplate.CodeDomProvider.AssemblyString) && !string.IsNullOrEmpty(xmlTemplate.CodeDomProvider.ClassName) && !string.IsNullOrEmpty(xmlTemplate.CodeDomProvider.CompilerVersion))
                    this.CodeDomProvider = new CodeDomProviderInfo() { AssemblyString = xmlTemplate.CodeDomProvider.AssemblyString, ClassName = xmlTemplate.CodeDomProvider.ClassName, CompilerVersion = xmlTemplate.CodeDomProvider.CompilerVersion };
            }
        }

        private void ReadAndBuildInputComponentCriteria(TemplateDef xmlTemplate)
        {
            this.InputComponentCriteria = null;
            if (xmlTemplate.InputComponentCriteria != null)
            {
                if (!string.IsNullOrEmpty(xmlTemplate.InputComponentCriteria.BooleanExpression) && !string.IsNullOrEmpty(xmlTemplate.InputComponentCriteria.Name) && !string.IsNullOrEmpty(xmlTemplate.InputComponentCriteria.Description))
                    this.InputComponentCriteria = new InputComponentCriteriaInfo() { BooleanExpression = xmlTemplate.InputComponentCriteria.BooleanExpression, Name = xmlTemplate.InputComponentCriteria.Name, Description = xmlTemplate.InputComponentCriteria.Description };
            }
        }       

        #endregion

        #region COMMENTED - read XML via DataSet.

        //private void ReadDataSet(DataSet ds)
        //{
        //    ReadHeader(ds);
        //    ReadVariables(ds);
        //    ReadSettings(ds);
        //    ReadTemplateSegments(ds);
        //    ReadReferencedAssemblies(ds);
        //    ReadNamespaces(ds);
        //    ReadComponentBuilder(ds);
        //    ReadCodeDomProvider(ds);
        //    ReadInputComponentCriteria(ds);
        //}

        //private void ReadHeader(DataSet ds)
        //{
        //    DataRow rowHeader = ds.Tables["Header"].Rows[0];
        //    this.Header = new TemplateHeader()
        //    {
        //        Title = Convert.ToString(rowHeader["Title"], CultureInfo.CurrentCulture),
        //        Description = Convert.ToString(rowHeader["Description"], CultureInfo.CurrentCulture),
        //        Family = Convert.ToString(rowHeader["Family"], CultureInfo.InvariantCulture)
        //    };
        //}

        //private void ReadVariables(DataSet ds)
        //{
        //    this.Variables = new Dictionary<string, TemplateVariableInfo>();
        //    if (ds.Tables.Contains("Variable"))
        //    {
        //        foreach (DataRow rowVariable in ds.Tables["Variable"].Rows)
        //        {
        //            NameValuePair[] allowedValues = ReadListItemsForVariable(ds, rowVariable);
        //            TemplateVariableInfo var = new TemplateVariableInfo(GetRowField(rowVariable, "Name"), GetRowField(rowVariable, "DefaultValue"), GetRowField(rowVariable, "Description"), allowedValues);
        //            this.Variables[var.Name] = var;
        //        }
        //    }
        //}

        //private static NameValuePair[] ReadListItemsForVariable(DataSet ds, DataRow rowVariable)
        //{
        //    List<NameValuePair> allowedValues = new List<NameValuePair>();
        //    foreach (DataRow rowListItem in GetListItemRows(ds, rowVariable))
        //    {
        //        allowedValues.Add(new NameValuePair(GetRowField(rowListItem, "Name"), GetRowField(rowListItem, "Value")));
        //    }

        //    return allowedValues.ToArray();
        //}

        //private static DataRow[] GetListItemRows(DataSet ds, DataRow rowVariable)
        //{
        //    DataRow rowAllowedValues = GetAllowedValuesRow(ds, rowVariable);
        //    if (rowAllowedValues != null)
        //    {
        //        DataRelation fkAllowedValues = ArrayUtil.Find<DataRelation>(ds.Relations, (rel) => (rel.ParentTable.TableName == rowAllowedValues.Table.TableName));
        //        if (fkAllowedValues != null)
        //            return rowAllowedValues.GetChildRows(fkAllowedValues);
        //    }

        //    return new DataRow[0];
        //}

        //private static DataRow GetAllowedValuesRow(DataSet ds, DataRow rowVariable)
        //{
        //    DataRelation fkVariable = ArrayUtil.Find<DataRelation>(ds.Relations, (rel) => (rel.ParentTable.TableName == "Variable"));
        //    if (fkVariable != null)
        //    {
        //        DataRow[] allowedValues = rowVariable.GetChildRows(fkVariable);
        //        if (allowedValues.Length > 0)
        //            return allowedValues[0];
        //    }

        //    return null;
        //}

        //private void ReadSettings(DataSet ds)
        //{
        //    this.Settings = new Dictionary<string, TemplateSetting>();
        //    if (ds.Tables.Contains("Setting"))
        //    {
        //        foreach (DataRow rowSetting in ds.Tables["Setting"].Rows)
        //        {
        //            TemplateSetting setting = new TemplateSetting() { Name = GetRowField(rowSetting, "Name"), Value = GetRowField(rowSetting, "Value"), Description = GetRowField(rowSetting, "Description") };
        //            this.Settings[setting.Name] = setting;
        //        }
        //    }
        //}

        //private void ReadTemplateSegments(DataSet ds)
        //{
        //    List<TemplateSegment> segments = new List<TemplateSegment>();
        //    foreach (DataRow rowSegment in ds.Tables["TemplateSegment"].Rows)
        //    {
        //        TemplateSegment segment = new TemplateSegment()
        //        {
        //            Id = Convert.ToString(rowSegment["Id"], CultureInfo.InvariantCulture),
        //            Body = Convert.ToString(rowSegment["TemplateSegment_Text"], CultureInfo.CurrentCulture)
        //        };

        //        segments.Add(segment);
        //    }

        //    this.segments = new TemplateSegmentCollection(segments);
        //}

        //private void ReadReferencedAssemblies(DataSet ds)
        //{
        //    List<string> assemblies = new List<string>();
        //    if (ds.Tables.Contains("Assembly"))
        //    {
        //        foreach (DataRow asmRow in ds.Tables["Assembly"].Rows)
        //        {
        //            string name = Convert.ToString(asmRow[0], CultureInfo.InvariantCulture);
        //            assemblies.Add(name);
        //        }
        //    }
        //    else if (ds.Tables.Contains("ReferencedAssemblies"))
        //    {
        //        if (ds.Tables["ReferencedAssemblies"].Columns.Contains("Assembly"))
        //        {
        //            foreach (DataRow asmRow in ds.Tables["ReferencedAssemblies"].Rows)
        //                assemblies.Add(Convert.ToString(asmRow["Assembly"], CultureInfo.InvariantCulture));
        //        }
        //    }

        //    this.ReferencedAssemblies = assemblies.ToArray();
        //}

        //private void ReadNamespaces(DataSet ds)
        //{
        //    List<string> namespaces = new List<string>();
        //    if (ds.Tables.Contains("Using"))
        //    {
        //        foreach (DataRow rowNamespace in ds.Tables["Using"].Rows)
        //        {
        //            string name = Convert.ToString(rowNamespace[0], CultureInfo.InvariantCulture);
        //            namespaces.Add(name);
        //        }
        //    }
        //    else if (ds.Tables.Contains("Namespaces"))
        //    {
        //        if (ds.Tables["Namespaces"].Columns.Contains("Using"))
        //        {
        //            foreach (DataRow rowNamespace in ds.Tables["Namespaces"].Rows)
        //                namespaces.Add(Convert.ToString(rowNamespace["Using"], CultureInfo.InvariantCulture));
        //        }
        //    }

        //    this.Namespaces = namespaces.ToArray();
        //}

        //private void ReadComponentBuilder(DataSet ds)
        //{
        //    this.componentBuilder = null;
        //    if (ds.Tables.Contains("InputComponentBuilder"))
        //    {
        //        DataRow rowComponentBuilder = ds.Tables["InputComponentBuilder"].Rows[0];
        //        string builderAssembly = Convert.ToString(rowComponentBuilder["Assembly"], CultureInfo.InvariantCulture);
        //        string builderClass = Convert.ToString(rowComponentBuilder["Class"], CultureInfo.InvariantCulture);
        //        if (!string.IsNullOrEmpty(builderAssembly) && !string.IsNullOrEmpty(builderClass))
        //            this.componentBuilder = (ITemplateInputComponentBuilder)ReflectionUtil.CreateInstance(builderAssembly, builderClass);
        //    }
        //}

        //private void ReadCodeDomProvider(DataSet ds)
        //{
        //    this.CodeDomProvider = null;
        //    if (ds.Tables.Contains("CodeDomProvider"))
        //    {
        //        DataRow rowCompiler = ds.Tables["CodeDomProvider"].Rows[0];
        //        string asm = GetRowField(rowCompiler, "AssemblyString");
        //        string cls = GetRowField(rowCompiler, "ClassName");
        //        string ver = GetRowField(rowCompiler, "CompilerVersion");
        //        if (!string.IsNullOrEmpty(asm) && !string.IsNullOrEmpty(cls) && !string.IsNullOrEmpty(ver))
        //            this.CodeDomProvider = new CodeDomProviderInfo() { AssemblyString = asm, ClassName = cls, CompilerVersion = ver };
        //    }
        //}

        //private void ReadInputComponentCriteria(DataSet ds)
        //{
        //    this.InputComponentCriteria = null;
        //    if (ds.Tables.Contains("InputComponentCriteria"))
        //    {
        //        DataRow rowCriteria = ds.Tables["InputComponentCriteria"].Rows[0];
        //        string expression = GetRowField(rowCriteria, "BooleanExpression");
        //        string name = GetRowField(rowCriteria, "Name");
        //        string desc = GetRowField(rowCriteria, "Description");
        //        if (!string.IsNullOrEmpty(expression) && !string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(desc))
        //            this.InputComponentCriteria = new InputComponentCriteriaInfo() { BooleanExpression = expression, Name = name, Description = desc };
        //    }
        //}

        //private static string GetRowField(DataRow row, string column)
        //{
        //    int fieldIdx = row.Table.Columns.IndexOf(column);
        //    if (fieldIdx < 0)
        //        return null;
        //    else
        //        return Convert.ToString(row[fieldIdx], CultureInfo.CurrentCulture);
        //}

        #endregion

        #region GetVariable, GetSetting.

        /// <summary>Gets the value stored in the specified variable.</summary>
        /// <param name="variableName">Variable name.</param>
        /// <returns>Value stored in the specified variable; <b>null</b> if the specified variable is not found.</returns>
        public string GetVariable(string variableName)
        {
            TemplateVariableInfo variable;
            this.Variables.TryGetValue(variableName, out variable);
            string value = (variable != null) ? variable.Value : null;
            return value;
        }

        /// <summary>Gets the value associated with the specified settings key.</summary>
        /// <param name="settingKey">Settings key.</param>
        /// <returns>Value associated with the specified settings key; <b>null</b> if the specified setting is not found.</returns>
        public string GetSetting(string settingKey)
        {
            TemplateSetting setting;
            this.Settings.TryGetValue(settingKey, out setting);
            string value = (setting != null) ? setting.Value : null;
            return value;
        }

        #endregion

        #region Run, IsValidInputComponent.

        /// <summary>Validates if provided input component is compatibiles with current template.</summary>
        public bool IsValidInputComponent(ITemplateInputComponent input)
        {
            if (this.InputComponentCriteria == null)
                return true;

            EnsureTemplateIsCompiled();
            string isMatch = EvalExpression(input, this.InputComponentCriteria.BooleanExpression).ToLowerInvariant();
            return (isMatch != "false");
        }

        /// <summary>Validates if provided object is compatibiles with current template.</summary>
        public bool IsValidInputComponent(object obj)
        {
            ITemplateInputComponent input = CreateComponent(obj);
            return IsValidInputComponent(input);
        }

        /// <summary>Checks whether a component variable/parameter is applicable to the specified input component.</summary>
        /// <param name="componentVariableName">Component variable name.</param>
        /// <param name="inputComponent">Input component to be tested againts used for boolean expression specifed in XML template.</param>
        /// <returns>Value returned by expression. <b>true</b> if expression is not defined in XML template or if the specified variable does not exist.</returns>
        public bool IsComponentVariableUsedFor(string componentVariableName, ITemplateInputComponent inputComponent)
        {
            foreach (TemplateComponentVariableDefinition componentVariable in this.ComponentVariableDefinitions)
            {
                if (componentVariable.Name == componentVariableName)
                    return IsComponentVariableUsedFor(componentVariable, inputComponent);
            }

            return true;
        }

        /// <summary>Checks whether a component variable/parameter is applicable to the specified input component.</summary>
        /// <param name="componentVariable">Component variable.</param>
        /// <param name="inputComponent">Input component to be tested againts used for boolean expression specifed in XML template.</param>
        /// <returns>Value returned by expression. <b>true</b> if expression is not defined in XML template.</returns>
        public bool IsComponentVariableUsedFor(TemplateComponentVariableDefinition componentVariable, ITemplateInputComponent inputComponent)
        {
            if (string.IsNullOrEmpty(componentVariable.UsedForBooleanExpression))
                return true;

            EnsureTemplateIsCompiled();
            string isMatch = EvalExpression(inputComponent, componentVariable.UsedForBooleanExpression).ToLowerInvariant();
            return (isMatch != "false");
        }

        /// <summary>Gets all component variables applicable to specified input component.</summary>
        /// <param name="inputComponent">Input component.</param>
        /// <returns>A collection of component variables.</returns>
        public IEnumerable<TemplateComponentVariableDefinition> GetComponentVariablesUsedFor(ITemplateInputComponent inputComponent)
        {
            foreach (TemplateComponentVariableDefinition componentVariable in this.ComponentVariableDefinitions)
            { 
                if (IsComponentVariableUsedFor(componentVariable, inputComponent))
                    yield return componentVariable;
            }
        }

        /// <summary>Compiles template and generates code for the given input component.</summary>
        public string Run(ITemplateInputComponent input)
        {
            if (this.segments.RootSegment == null)
                throw new InvalidOperationException("The root segment is not set.");

            EnsureTemplateIsCompiled();
            input.CurrentTemplateData = this;
            return Generate(this.segments.RootSegment, input);
        }

        /// <summary>Compiles template and generates code for the given object.</summary>
        public string Run(object obj)
        {
            ITemplateInputComponent input = CreateComponent(obj);
            return Run(input);
        }

        /// <summary>Compiles template and generates code for the given input component.</summary>
        public void Run(StreamWriter output, ITemplateInputComponent input)
        {
            if (this.segments.RootSegment == null)
                throw new InvalidOperationException("The root segment is not set.");

            EnsureTemplateIsCompiled();
            input.CurrentTemplateData = this;
            Generate(output, this.segments.RootSegment, input);
        }

        /// <summary>Compiles template and generates code for the given object.</summary>
        public void Run(StreamWriter output, object obj)
        {
            ITemplateInputComponent input = CreateComponent(obj);
            Run(output, input);
        }

        private ITemplateInputComponent CreateComponent(object instance)
        {
            if (this.componentBuilder == null)
                throw new InvalidOperationException("Input component builder is not defined. It should be defined in 'InputComponentBuilder' section of template's XML file.");

            return this.componentBuilder.Build(instance);
        }

        #endregion

        #region Compile.

        TemplateExpressionFactory compiledExpressionFactory;     

        private void EnsureTemplateIsCompiled()
        {
            if (this.compiledExpressionFactory == null)
                this.compiledExpressionFactory = TemplateCompiler.CompileExpressions(this);
        }

        #endregion

        #region Generate, Write - internal methods.

        /// <summary>Generates code segments for the given input components.</summary>
        internal string Generate(string segmentId, IEnumerable<ITemplateInputComponent> components)
        {
            TemplateSegment segment = GetSegment(segmentId, /*throw exception if not found*/ true);
            StringBuilder output = new StringBuilder();
            foreach (ITemplateInputComponent component in components)
                output.Append(Generate(segment, component));

            return output.ToString();
        }

        /// <summary>Generates code segments for the given input compoments if the specified condition is met.</summary>
        internal string GenerateIf(string segmentId, IEnumerable<ITemplateInputComponent> components, ITemplateInputComponent criteriaParameter, params Predicate<ITemplateInputComponent>[] criteria)
        {
            if (!MatchesCriteria(criteriaParameter, criteria))
                return "";

            TemplateSegment segment = GetSegment(segmentId, /*throw exception if not found*/ true);
            StringBuilder output = new StringBuilder();
            foreach (ITemplateInputComponent component in components)
                output.Append(Generate(segment, component));

            return output.ToString();
        }

        /// <summary>Generates code segments for input components which match the specified criteria.</summary>
        internal string Generate(string segmentId, ITemplateInputComponent component, params Predicate<ITemplateInputComponent>[] componentCriteria)
        {
            TemplateSegment segment = GetSegment(segmentId, /*throw exception if not found*/ true);
            return Generate(segment, component, componentCriteria);
        }

        /// <summary>Generates code segments for input components which match the specified criteria.</summary>
        internal string Generate(string segmentId, ITemplateInputComponent component, params bool[] evaluatedComponentCriteria)
        {
            TemplateSegment segment = GetSegment(segmentId, /*throw exception if not found*/ true);
            if (AllVariablesAreTrue(evaluatedComponentCriteria))
                return Generate(segment, component);
            else
                return "";
        }

        private static bool AllVariablesAreTrue(bool[] variables)
        {
            foreach (bool isTrue in variables)
            {
                if (!isTrue)
                    return false;
            }

            return true;
        }

        /// <summary>Writes the text if the input component matches the specified criteria.</summary>
        internal static string WriteIf(string text, ITemplateInputComponent component, params Predicate<ITemplateInputComponent>[] componentCriteria)
        {
            bool shouldGenerate = ArrayUtil.IsNullOrEmpty(componentCriteria) || MatchesCriteria(component, componentCriteria);
            if (!shouldGenerate)
                return "";
            else
                return text ?? "";
        }

        /// <summary>Writes the text if the input component matches the specified criteria.</summary>
        internal static string WriteIf(string text, params bool[] evaluatedCriteria)
        {
            if (AllVariablesAreTrue(evaluatedCriteria))
                return text ?? "";
            else
                return "";
        }

        private TemplateSegment GetSegment(string segmentId, bool throwExceptionIfNotFound)
        {
            var seg = this.segments.GetSegment(segmentId);
            if (throwExceptionIfNotFound && (seg == null))
                throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Template '{0}' does not contain segment '{1}'.", this.Header.Title, segmentId));

            return seg;
        }

        /// <summary>Generates code segment for the given input compoments if it matches specified criteria.</summary>
        private string Generate(TemplateSegment segment, ITemplateInputComponent component, params Predicate<ITemplateInputComponent>[] componentCriteria)
        {
            if (component == null)
                return "";
            if (segment == null)
                throw new ArgumentNullException("segment", "Segment may not be null.");

            if (IsDebugConditionMatched(segment, component))
                Debugger.Break();

            bool shouldGenerate = ArrayUtil.IsNullOrEmpty(componentCriteria) || MatchesCriteria(component, componentCriteria);
            if (!shouldGenerate)
                return "";

            return ParseAndGenerateSegment(segment, component);
        }

        private static bool IsDebugConditionMatched(TemplateSegment segment, ITemplateInputComponent component)
        {
            bool shouldDebug = false;
            if (segment.Debug)
            {
                if (string.IsNullOrEmpty(segment.DebugIfInputName))
                    shouldDebug = true;
                else if (component.Name == segment.DebugIfInputName)
                    shouldDebug = true;
            }

            return shouldDebug;
        }

        /// <summary>Generates code segment for the given input compoments if it matches specified criteria.</summary>
        private void Generate(StreamWriter output, TemplateSegment segment, ITemplateInputComponent component, params Predicate<ITemplateInputComponent>[] componentCriteria)
        {
            if (component == null)
                return;
            if (segment == null)
                throw new ArgumentNullException("segment", "Segment may not be null.");

            if (IsDebugConditionMatched(segment, component))
                Debugger.Break();

            bool shouldGenerate = ArrayUtil.IsNullOrEmpty(componentCriteria) || MatchesCriteria(component, componentCriteria);
            if (!shouldGenerate)
                return;

            ParseAndGenerateSegment(output, segment, component);
        }

        #endregion

        #region Parse template segments, eval expressions, write literals - private.

        private string ParseAndGenerateSegment(TemplateSegment segment, ITemplateInputComponent component)
        {
            StringBuilder output = new StringBuilder();
            var orderedSegmentParts = segment.Parse();
            foreach (SegmentPart part in orderedSegmentParts)
            {
                if (part.IsExpression)
                {
                    string expressionResult = EvalExpression(component, part.Body);
                    output.Append(expressionResult);
                }
                else
                {
                    output.Append(part.Body);
                }
            }

            return output.ToString();
        }

        private void ParseAndGenerateSegment(StreamWriter output, TemplateSegment segment, ITemplateInputComponent component)
        {
            var orderedSegmentParts = segment.Parse();
            foreach (SegmentPart part in orderedSegmentParts)
            {
                if (part.IsExpression)
                {
                    string expressionResult = EvalExpression(component, part.Body);
                    output.Write(expressionResult);
                }
                else
                {
                    output.Write(part.Body);
                }
            }
        }        

        private string EvalExpression(ITemplateInputComponent component, string expressionSource)
        {
            ITemplateExpression expression = CreateTemplateExpression(component, expressionSource);
            string output;
            try
            {
                output = expression.Eval();
            }
            catch (ExpressionEvaluationException previousExceptionInStack)
            {
                throw new ExpressionEvaluationException(expressionSource, previousExceptionInStack);
            }
            catch (Exception evalException)
            {
                throw new ExpressionEvaluationException(expressionSource, evalException, this, component);                
            }

            return output;
        }

        private ITemplateExpression CreateTemplateExpression(ITemplateInputComponent component, string expressionSource)
        {
            ITemplateExpression expression = this.compiledExpressionFactory.NewTemplateExpression(expressionSource);
            expression.Initialize(this, component);
            return expression;
        }

        #endregion

        #region Component criteria utility methods - internal.

        internal static IEnumerable<ITemplateInputComponent> GetMatchingComponents(IEnumerable<ITemplateInputComponent> components, Predicate<ITemplateInputComponent>[] criteria)
        {
            if (ArrayUtil.IsNullOrEmpty(criteria))
                return components;

            List<ITemplateInputComponent> matches = new List<ITemplateInputComponent>();
            foreach (ITemplateInputComponent component in components)
            {
                if (MatchesCriteria(component, criteria))
                    matches.Add(component);
            }

            return matches;
        }

        internal static bool MatchesCriteria(ITemplateInputComponent component, Predicate<ITemplateInputComponent>[] criteria)
        {
            foreach (Predicate<ITemplateInputComponent> matchesCurrent in criteria)
            {
                if (!matchesCurrent(component))
                    return false;
            }

            return true;
        }

        #endregion
    }

    [Serializable]
    internal sealed class ExpressionEvaluationException : Exception
    {
        #region Members.

        string expression;
        ExpressionEvaluationException previousExceptionInStack;
        ITemplate template; 
        ITemplateInputComponent inputComponent;

        #endregion

        #region Constructors.

        public ExpressionEvaluationException(string expression, Exception evalException, ITemplate template, ITemplateInputComponent inputComponent)
            : base(evalException.Message, evalException)
        {
            this.expression = expression;
            this.template = template;
            this.inputComponent = inputComponent;
        }

        public ExpressionEvaluationException(string expression, ExpressionEvaluationException previousExceptionInStack)
        {
            this.expression = expression;
            this.previousExceptionInStack = previousExceptionInStack;
            this.template = previousExceptionInStack.template;
            this.inputComponent = previousExceptionInStack.inputComponent;
        }

        #endregion

        #region Overriden properties and methods.

        public override string Message
        {
            get { return ToString(); }
        }

        public override string ToString()
        {
            StringBuilder errmsg = new StringBuilder();
            if (this.template != null)
                errmsg.AppendLine("Template: " + template.Header.Title);

            if (this.inputComponent != null)
            {
                errmsg.AppendLine("Current component: " + this.inputComponent.Name);
                if (this.inputComponent.Parent != null)
                    errmsg.AppendLine("Parent component: " + this.inputComponent.Parent.Name);

                if (this.inputComponent.Root != null)
                    errmsg.AppendLine("Root component: " + this.inputComponent.Root.Name);                               
            }

            errmsg.AppendLine("Exception occured while evaluating the following expression stack:");
            AppendErrorDetails(errmsg, this);
            return errmsg.ToString();
        }

        private static void AppendErrorDetails(StringBuilder errmsg, ExpressionEvaluationException expressionException)
        {
            errmsg.Append("   ");
            errmsg.AppendLine(expressionException.expression);
            if (expressionException.previousExceptionInStack != null)
            {
                AppendErrorDetails(errmsg, expressionException.previousExceptionInStack);
            }
            else if (expressionException.InnerException != null)
            {
                errmsg.AppendLine(expressionException.InnerException.ToString());
            }
        }

        #endregion
    }
}